/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.modeling;

import java.util.ArrayList;

import javax.media.opengl.GL;

import de.akob.misc.OpenGLDrawable;

/**
 * Class for complex OpenGL objects consisting of many primitives and different materials.
 */
public class GLModel implements OpenGLDrawable {
	// object data
	private ArrayList<double[]>         vertices;
	private ArrayList<double[]>         texels;
	private ArrayList<double[]>         normals;
	
	// data for faces, materials, ...
	private ArrayList<ArrayList<int[]>> primitiveClusters_vertices;
	private ArrayList<ArrayList<int[]>> primitiveClusters_texels;
	private ArrayList<ArrayList<int[]>> primitiveClusters_normals;
	private ArrayList<GLMaterial>       primitiveClusters_materials;
	
	// display list for object
	private int                         displayList;
	
	// whether this this model supports shading (or whether it is requested)
	private boolean                     shadable           = true;
	private boolean                     shadable_requested = true;
	
	/**
	 * Constructs an GLObject using the given vertices, texels, normals and primitive clusters. Primitive clusters
	 * consists of several arraylists that carry data for a bunch of polygons (faces, texels, ...). The vertices, texels
	 * and normals primitive clusters are integer array that reference to indices for the vertices, texels and normals
	 * arrays.
	 */
	public GLModel(ArrayList<double[]> vertices, ArrayList<double[]> texels, ArrayList<double[]> normals,
	        ArrayList<ArrayList<int[]>> pc_vertices, ArrayList<ArrayList<int[]>> pc_texels,
	        ArrayList<ArrayList<int[]>> pc_normals, ArrayList<GLMaterial> pc_materials) {
		this.vertices = vertices;
		this.texels = texels;
		this.normals = normals;
		this.primitiveClusters_vertices = pc_vertices;
		this.primitiveClusters_texels = pc_texels;
		this.primitiveClusters_normals = pc_normals;
		this.primitiveClusters_materials = pc_materials;
	}
	
	/**
	 * Draws this object. If the display list of this object is not yet updated this method will call
	 * updateDisplayList() prior to drawing the object.
	 */
	public void draw(GL gl) {
		if (displayList == 0 || shadable != shadable_requested)
			updateDisplayList(gl);
		gl.glCallList(displayList);
	}
	
	/**
	 * Returns the current display list of the model.
	 */
	public int getDisplayList() {
		return displayList;
	}
	
	/**
	 * Returns whether this object is shadable (using volume shaders). Objects supporting volume shaders have to provide
	 * additional vertices.
	 * 
	 * @see #renderEdgeQuadriliterals
	 */
	public boolean isShadable() {
		return shadable;
	}
	
	/**
	 * Will render every edge in the given vertex data as a quadriliteral (GL_QUADS). If you want to use shading by
	 * volume shadows (using the shadow volume shader) with a model, it has to provide additional 'zero-width' faces
	 * (namely every edge has to be transformed into a face). This is due to the way the shadow volume is created by the
	 * shader). This method will render these edges (you should use it to create a display list).
	 */
	private void renderEdgeQuadriliterals(GL gl) {
		// create single array of all faces (easier to access; primitive clusters are of no importance here)
		ArrayList<int[]> faces = new ArrayList<int[]>();
		ArrayList<int[]> normalFaces = new ArrayList<int[]>();
		for (int i = 0; i < primitiveClusters_vertices.size(); i++) {
			ArrayList<int[]> faces_v = primitiveClusters_vertices.get(i);
			ArrayList<int[]> faces_n = primitiveClusters_normals.get(i);
			for (int j = 0; j < faces_v.size(); j++) {
				faces.add(faces_v.get(j));
				normalFaces.add(faces_n.get(j));
			}
		}
		
		// draw quadriliterals for every edge
		gl.glBegin(GL.GL_QUADS);
		for (int i = 0; i < faces.size(); i++) {
			for (int j = i + 1; j < faces.size(); j++) {
				int[] face1 = faces.get(i);
				int[] face2 = faces.get(j);
				int[] nFace1 = normalFaces.get(i);
				int[] nFace2 = normalFaces.get(j);
				for (int k = 0; k < face1.length; k++) {
					for (int l = 0; l < face2.length; l++) {
						int idx11 = k, idx21 = l;
						int idx12 = (k + 1) % face1.length, idx22 = (l + 1) % face2.length;
						if (face1[idx11] == face2[idx22] && face1[idx12] == face2[idx21]) {
							gl.glNormal3dv(normals.get(nFace2[idx22]), 0);
							gl.glVertex4dv(vertices.get(face2[idx22]), 0);
							gl.glNormal3dv(normals.get(nFace2[idx21]), 0);
							gl.glVertex4dv(vertices.get(face2[idx21]), 0);
							gl.glNormal3dv(normals.get(nFace1[idx12]), 0);
							gl.glVertex4dv(vertices.get(face1[idx12]), 0);
							gl.glNormal3dv(normals.get(nFace1[idx11]), 0);
							gl.glVertex4dv(vertices.get(face1[idx11]), 0);
						}
					}
				}
			}
		}
		gl.glEnd();
	}
	
	/**
	 * Sets whether this model can be used for shadow volumes. Models need additional vertices for use of shadow
	 * volumes. Be aware that this model will cause a large delay as soon as the model is redrawn (but only once).
	 * 
	 * @see #renderEdgeQuadriliterals
	 */
	public void setShadable(boolean value) {
		this.shadable_requested = value;
	}
	
	/**
	 * Updates the display list of the object according to its current data.
	 */
	public void updateDisplayList(GL gl) {
		// create and start new display list
		displayList = gl.glGenLists(1);
		gl.glNewList(displayList, GL.GL_COMPILE);
		
		// save some parts of the OpenGL state
		gl.glPushAttrib(GL.GL_TEXTURE_BIT);
		
		// render the different primitive clusters
		for (int i = 0; i < primitiveClusters_vertices.size(); i++) {
			ArrayList<int[]> faces_v = primitiveClusters_vertices.get(i);
			ArrayList<int[]> faces_t = primitiveClusters_texels.get(i);
			ArrayList<int[]> faces_n = primitiveClusters_normals.get(i);
			GLMaterial material = primitiveClusters_materials.get(i);
			
			if (faces_v.size() == 0)
				continue;
			int PRIMITIVE_TYPE;
			if (faces_v.get(0).length == 3)
				PRIMITIVE_TYPE = GL.GL_TRIANGLES;
			else if (faces_v.get(0).length == 4)
				PRIMITIVE_TYPE = GL.GL_QUADS;
			else
				PRIMITIVE_TYPE = GL.GL_POLYGON;
			
			// load material of current primitive cluster
			if (material != null)
				material.enable(gl);
			
			// draw the current primitive cluster
			if (PRIMITIVE_TYPE != GL.GL_POLYGON)
				gl.glBegin(PRIMITIVE_TYPE);
			for (int j = 0; j < faces_v.size(); j++) {
				int[] face_v = faces_v.get(j);
				int[] face_t = (faces_t == null) ? null : faces_t.get(j);
				int[] face_n = (faces_n == null) ? null : faces_n.get(j);
				
				// draw face
				if (PRIMITIVE_TYPE == GL.GL_POLYGON)
					gl.glBegin(PRIMITIVE_TYPE);
				for (int k = 0; k < face_v.length; k++) {
					if (face_n != null)
						gl.glNormal3dv(normals.get(face_n[k]), 0);
					if (face_t != null)
						gl.glTexCoord3dv(texels.get(face_t[k]), 0);
					gl.glVertex4dv(vertices.get(face_v[k]), 0);
				}
				if (PRIMITIVE_TYPE == GL.GL_POLYGON)
					gl.glEnd();
			}
			if (PRIMITIVE_TYPE != GL.GL_POLYGON)
				gl.glEnd();
		}
		
		// if requested, draw extra edges for shading by use of volume shadows
		if (isShadable())
			renderEdgeQuadriliterals(gl);
		
		// restore saved OpenGL state parameters
		gl.glPopAttrib();
		
		// end display list
		gl.glEndList();
	}
}
